package com.ease.util {

import flash.utils.Dictionary;

import mx.utils.StringUtil;

/**
 * A key-value based object spec or filter. The filter is based on properties and
 * meta-properties of the objects of interest. It also contains optional (client supplied)
 * information about the context in which objects are being searched, which can be used
 * for further filtering of the matching objects.
 *
 * <p>A property is accessible from the object and can be direct or indirect (i.e.,
 * nested). A meta-property, on the other hand, is not available on the object itself but
 * rather is assigned and interpreted by some other service in the system. contained in
 * this. Keys in the key-value pairs are names of the property or meta-property in
 * question. The overall filter is evaluated by <i>and</i>ing together all the individual
 * property and meta-property filters.
 */
public class ObjectFilter {
   /**
    * The regex used to tokenize expressions of the form: x1=y1 and x2=y2 ... xN=yN
    */
   private const PREDICATE_EXPR_SPLITTER_REGEX:RegExp =
         /([^=\s,]+)\s*=\s*([^=\s,]+)\s*(?:,|and|$)\s*/;
   private var _propFilters:Dictionary = new Dictionary();
   private var _metaPropFilters:Dictionary = new Dictionary();
   private var _selectionContextToken:String;

   /**
    * An option for methods like filter, which specifies that duplicate members of a
    * collection should be discarded.
    */
   public static const DISCARD_DUPLICATES:uint = 1 << 0;

   //-------------------------------------------------------
   // Property: metaPropertyFilterCount

   private var _metaPropertyFilterCount:int = 0;

   /**
    * The number of meta-property filters defined in this spec.
    */
   public function get metaPropertyFilterCount():int {
      return _metaPropertyFilterCount;
   }

   /**
    * The type of the objects that this spec is targeting.
    */
   public var type:Class;

   /**
    * Specifies the lifetime or creation policy for the target objects. Possible values
    * include "keepalive", "createnew" and "lookuponly".
    */
   public var policy:String;

   //-------------------------------------------------------
   // Property: selectionContext

   private var _selectionContext:Object;

   /**
    * Specifies the context for the object selection or filtering process.
    *
    * <p>The client can use it to provide some arbitrary data representing the context in
    * which objects are being looked up. This data typically contains properties of the
    * client object (i.e., the consumer of the queried objects), as opposed to the
    * properties of the queried objects. This information can be used by an object
    * provider/directory to select/filter the set of applicable objects.
    */
   public function get selectionContext():Object {
      return _selectionContext;
   }

   /**
    * Sets the value of this object's selectionContext property. An optional token string
    * can provided to represent the property, such that the selection context gets
    * reassessed using this token everytime reassessValueTokensBasedOnObjectProperties is
    * called on this spec. If such a token is not supplied and the value is an
    * ObjectProjection then the selection context is reassessed using the projection.
    */
   public final function setSelectionContext( value:Object,
                                              valueToken:String = null ):void {
      _selectionContext = value;
      _selectionContextToken = valueToken;
   }

   /**
    * Adds to this object filter the property and/or meta-property filters encoded in the
    * given predicate expression.
    *
    * @param expr
    *    A predicate expression having one of the following forms:
    *    <p>   key1=val1 and key2=val2 and ... and keyN=valN </p>
    *    <p>   key1=val1, key2=val2, ..., keyN=valN </p>
    *    <p>where extra white spaces around the keys and values are ignored. If a key
    *    begins with '&#64;', it's assumed to represent a meta-property.</p>
    *
    * @param valueRefResolver
    *    The object to be used to resolve tokens or value references, i.e., a token of the
    *    form {foo} would be resolved as the value of this parameter's foo property.
    */
   public final function addFiltersUsingPredicateExpr( expr:String,
                                                       valueRefResolver:Object ):void {
      var parts:Array/*String*/ = expr.split( PREDICATE_EXPR_SPLITTER_REGEX );
      parts = MixedUtil.filterOut( parts, MixedUtil.isNullOrEmpty );
      if (parts.length % 2 != 0) { // length is odd
         Diagnostics.assert( false, "Malformed predicate: " + expr );
         return;
      }
      for (var i:int = 0; i < parts.length; i += 2) {
         var key:String = parts[i];
         var valueOrValueToken:String = parts[i+1];
         // change something like "'foo'" into "foo"
         valueOrValueToken = StringUtils.trimMatchingQuotesOnce( valueOrValueToken, "'" );
         if (key.charAt( 0 ) == '@') { // indicates meta-property
            key = key.substr( 1 );
            addMetaPropertyFilter( key, valueOrValueToken, valueRefResolver );
         } else {
            addPropertyFilter( key, valueOrValueToken, valueRefResolver );
         }
      }
   }

   /**
    * Adds a new property filter or constraint to this object filter.
    *
    * @param key
    *    Name of the property that is to be used for filtering objects.
    *
    * @param valueOrValueReference
    *    Represents the value; it's either the value itself, or a reference to the value.
    *    In the latter case, it's a substitution token (e.g., "{foo}") containing the name
    *    of the property (of the target object) whose value is the value to use for this
    *    filter.
    *
    * @param target
    *    If valueOrValueToken is a substitution token, the property would be read off of
    *    this target object.
    */
   public final function addPropertyFilter( key:String, valueOrValueToken:String,
                                            target:Object = null ):void {
      var value:Object = ReflectionUtil.resolveTagValue( valueOrValueToken, target );
      var valueToken:String;
      if (value != valueOrValueToken) {
         valueToken = valueOrValueToken;
      }
      addPropertyFilter_Internal( key, value, valueToken );
   }

   /**
    * Removes an existing property filter or constraint from this object filter.
    *
    * @param key
    *    Name of the property that is used for filtering objects.
    */
   public final function removePropertyFilter( key:String ):void {
      delete _propFilters[key];
   }

   /**
    * Indicates whether a property filter or constraint by the given key exists in this
    * object filter.
    *
    * @param key
    *    Name of the property that is used for filtering objects.
    */
   public final function containsPropertyFilter( key:String ):Boolean {
      return _propFilters.hasOwnProperty( key );
   }

   /**
    * Returns all the property keys that match the given pattern (e.g., a RegExp). If
    * pattern is null, all keys are returned.
    */
   public final function getPropertyKeys( pattern:* = null ):Array/*String*/ {
      var matchingKeys:Array/*String*/ = [];
      for (var key:String in _metaPropFilters) {
         if (pattern == null || key.search( pattern ) != -1) {
            matchingKeys.push( key );
         }
      }
      return matchingKeys;
   }

   /**
    * Returns the value specified in a property filter or constraint for the given
    * property name.
    */
   public final function getPropertyValue( key:String ):Object {
      var val:Object = getPropFilterSpecifiedValue( key );
      return val;
   }

   /**
    * Adds a new meta-property filter or constraint to this object filter.
    *
    * @param key
    *    Name of the meta-property that is to be used for filtering objects.
    *
    * @param valueOrValueReference
    *    Represents the value; it's either the value itself, or a reference to the value.
    *    In the latter case, it's a substitution token (e.g., "{foo}") containing the name
    *    of the property (of the target object) whose value is the value to use for this
    *    filter.
    *
    * @param target
    *    If valueOrValueToken is a substitution token, the property would be read off of
    *    this target object.
    */
   public final function addMetaPropertyFilter( key:String, valueOrValueToken:String,
                                                target:Object = null ):void {
      var value:Object = ReflectionUtil.resolveTagValue( valueOrValueToken, target );
      var valueToken:String;
      if (value != valueOrValueToken) {
         valueToken = valueOrValueToken;
      }
      addMetaPropertyFilter_Internal( key, value, valueToken );
   }

   /**
    * Removes an existing meta-property filter or constraint from this object filter.
    *
    * @param key
    *    Name of the meta-property that is used for filtering objects.
    */
   public final function removeMetaPropertyFilter( key:String ):void {
      if (! _metaPropFilters.hasOwnProperty( key )) {
         return;
      }
      _metaPropertyFilterCount--;
      delete _metaPropFilters[key];
   }

   /**
    * Indicates whether a meta-property filter or constraint by the given key exists in
    * this object filter.
    *
    * @param key
    *    Name of the meta-property that is used for filtering objects.
    */
   public final function containsMetaPropertyFilterFor( key:String ):Boolean {
      return _metaPropFilters.hasOwnProperty( key );
   }

   /**
    * Returns all the meta-property keys that match the given pattern (e.g., a RegExp).
    * If pattern is null, all keys are returned.
    */
   public final function getMetaPropertyKeys( pattern:* = null ):Array/*String*/ {
      var matchingKeys:Array/*String*/ = [];
      for (var key:String in _propFilters) {
         if (pattern == null || key.search( pattern ) != -1) {
            matchingKeys.push( key );
         }
      }
      return matchingKeys;
   }

   /**
    * Returns the value specified in a meta-property filter or constraint for the given
    * meta-property name.
    */
   public final function getMetaPropertyValue( key:String ):Object {
      var val:Object = _metaPropFilters[key];
      if (val is FilterValueInfo) {
         return FilterValueInfo( val ).currentValue;
      }
      return val;
   }

   /**
    * Return value indicates whether the reassessment resulted in any modifications in
    * the spec.
    */
   public final function reassessValueTokensBasedOnObjectProperties(
         object:Object ):Boolean {
      var isSpecModified:Boolean = false;
      var mappedValue:Object;
      var valueInfo:FilterValueInfo;
      var value:Object;

      // meta-property filters
      for each (mappedValue in _metaPropFilters) {
         if (! (mappedValue is FilterValueInfo)) {
            continue;
         }
         valueInfo = FilterValueInfo( mappedValue );
         value = ReflectionUtil.resolveTagValue( valueInfo.valueToken, object );
         if (valueInfo.currentValue != value) {
            isSpecModified = true;
            valueInfo.currentValue = value;
         }
      }

      // property filters
      for each (mappedValue in _propFilters) {
         if (! (mappedValue is FilterValueInfo)) {
            continue;
         }
         valueInfo = FilterValueInfo( mappedValue );
         value = ReflectionUtil.resolveTagValue( valueInfo.valueToken, object );
         if (valueInfo.currentValue != value) {
            isSpecModified = true;
            valueInfo.currentValue = value;
         }
      }

      // selection context
      if (_selectionContextToken != null) {
         value = ReflectionUtil.resolveTagValue( _selectionContextToken, object );
         if (_selectionContext != value) {
            isSpecModified = true;
            _selectionContext = value;
         }
      } else if (_selectionContext is ObjectProjection) {
         var isCtxModified:Boolean =
               ObjectProjection( _selectionContext ).evaluate( object );
         isSpecModified ||= isCtxModified;
      }

      return isSpecModified;
   }

   /**
    * Evaluates whether the given object passes this filter or not. Since meta-properties
    * do not live on the object itself, the caller can optionally supply a set of
    * functions to be invoked to evaluate the meta-properties for the given object.
    *
    * @param metaFilterEvaluators
    *     Callback methods of the form:
    *     <p><code>function foo( spec:ObjectFilter, object:* ):Boolean { ... }</code>
    *
    * @see #filter()
    */
   public final function passesFilter(
         object:Object, metaFilterEvaluators:Array/*Function*/ = null ):Boolean {
      for (var key:String in _propFilters) {
         var value:Object = getPropFilterSpecifiedValue( key );
         var propVal:* = ReflectionUtil.getNestedPropertyValue( object, key );
         // See if the current property value satisfies the given value constraint. Note
         // that the constraint value could be an array, having resulted from some token
         // evaluation.
         if (! MixedUtil.isOrContains( value, propVal )) {
            return false;
         }
      }
      if (metaFilterEvaluators != null) {
         for each (var evalFunc:Function in metaFilterEvaluators) {
            if (! evalFunc( this, object )) {
               return false;
            }
         }
      }
      return true;
   }

   /**
    * A convenience method to return a filtered version of the given array, which contains
    * only those objects that satisfy the property filter defined by this spec.
    * Meta-property filters are ignored by this method.
    *
    * @param options
    *    Various options for the filter, e.g., DISCARD_DUPLICATES.
    *
    * @see #passesFilter()
    */
   public final function filter(
         objects:Array/*?*/, options:uint = 0,
         metaFilterEvaluators:Array/*Function*/ = null ):Array/*?*/ {
      var filterFunctor:Function = function( item:*, index:int, array:Array ):Boolean {
         return passesFilter( item, metaFilterEvaluators );
      }
      var filteredObjects:Array = objects;

      // The property filter could be expensive, so first remove the duplicates (if
      // requested).
      var discardDuplicates:Boolean = (options & DISCARD_DUPLICATES) != 0
      if (discardDuplicates) {
         MixedUtil.removeDuplicates( filteredObjects );
      }
      filteredObjects = filteredObjects.filter( filterFunctor );

      return filteredObjects;
   }

   /**
    * Creates a new object filter which contains this filter's meta-property filters as
    * property filters.
    *
    * @param metaPropNamePrefix
    *    An optional meta-property name prefix. If specified, only those meta-properties
    *    are considered whose names contain this prefix. The resulting property filters
    *    do not contain the prefix part in their keys.
    */
   public final function
   newPropertyFilterFromMetaFilter( metaPropNamePrefix:String ):ObjectFilter {
      var prefixLength:int = metaPropNamePrefix.length;
      var thisClass:Class = Object( this ).constructor;
      var newSpec:ObjectFilter = new thisClass();
      for (var metaPropName:String in _metaPropFilters) {
         if (metaPropName.indexOf( metaPropNamePrefix ) == 0) {
            var propName:String = metaPropName.substr( prefixLength );
            newSpec.addPropertyFilter( propName, _metaPropFilters[metaPropName] );
         }
      }
      return newSpec;
   }

   /**
    * Sets all the properties on the given object that are specified in the property
    * filter spec, so as to make the object conform to this ObjectFilter. It suppresses
    * all the exceptions encountered while trying to set properties and merely returns a
    * flag indicating success or failure. It supports nested property names (i.e.,
    * properties of the form x.y...z, not just simple names like x).
    *
    * @returns
    *    Returns true if the given object is non-null and either already satisfies the
    *    property filter of this spec, or the requisite properties are successfully set on
    *    the object by this method. Returns false otherwise.
    */
   public final function setFilteredPropertiesOn( object:* ):Boolean {
      if (object == null) {
         return false;
      }
      for (var key:String in _propFilters) {
         var value:Object = getPropFilterSpecifiedValue( key );
         var propVal:*;
         try {
            propVal = ReflectionUtil.getNestedPropertyValue( object, key );
         } catch (err:Error) {
            // Couldn't even read the property value from the object!
            var msg:String = StringUtil.substitute( "ObjectFilter: couldn't get {0}.{1}",
                                                    object, key );
            trace( msg );
            return false;
         }
         // See if the current property value satisfies the value constraint on the
         // corresponding property of the object, as specified in this spec. Note that
         // the constraint value could be an array, having resulted from some token
         // evaluation.
         if (MixedUtil.isOrContains( value, propVal )) {
            continue;
         }
         // The current property value is no good, so we overwrite it.
         try {
            ReflectionUtil.setNestedPropertyValue( object, key, value );
            propVal = ReflectionUtil.getNestedPropertyValue( object, key );
            if (value == propVal) {
               continue;
            }
         } catch (err:Error) {
            // ignore; we try differently below
         }
         // Now try under the assumption that the spec'ed (or desired) value is an
         // indexable collection (such that any value in the collection could be used to
         // satisfy the spec).
         try {
            ReflectionUtil.setNestedPropertyValue( object, key, value[0] );
            propVal = ReflectionUtil.getNestedPropertyValue( object, key );
            if (MixedUtil.isOrContains( value, propVal )) {
               continue;
            }
         } catch (err:Error) {
            return false;
         }
      }
      return true;
   }

   /**
    * Adds a new property filter or constraint to this object filter.
    *
    * @param key
    *    Name of the property that is to be used for filtering objects.
    */
   private function addPropertyFilter_Internal( key:String, value:Object,
                                                valueToken:String = null ):void {
      if (valueToken != null) {
         _propFilters[key] = new FilterValueInfo( key, valueToken, value );
      } else {
         _propFilters[key] = value;
      }
   }

   /**
    * Adds a new meta-property filter or constraint to this object filter.
    *
    * @param key
    *    Name of the meta-property that is to be used for filtering objects.
    */
   private function addMetaPropertyFilter_Internal( key:String, value:Object,
                                                    valueToken:String = null ):void {
      if (! _metaPropFilters.hasOwnProperty( key )) {
         _metaPropertyFilterCount++;
      }
      if (valueToken) {
         _metaPropFilters[key] = new FilterValueInfo( key, valueToken, value );
      } else {
         _metaPropFilters[key] = value;
      }
   }

   private function getPropFilterSpecifiedValue( key:String ):Object {
      var value:Object = _propFilters[key];
      if (value is FilterValueInfo) {
         value = FilterValueInfo( value ).currentValue;
      }
      return value;
   }
} // class

}

/**
 * Represents the value targeted by a property filter (which is part of some
 * ObjectFilter). Contains the value itself, or if the specified value is a token, the
 * last resolved value of the token.
 */
class FilterValueInfo
{
   public var key:String;
   public var valueToken:String;
   public var currentValue:Object;

   public function FilterValueInfo( key:String, valueToken:String, currentValue:Object ) {
      this.key = key;
      this.valueToken = valueToken;
      this.currentValue = currentValue;
   }
}
